using System;
//using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using System.IO.Compression;
using System.Threading;
using System.ComponentModel;

namespace sCicero
{

    public class EnCryptoException : ApplicationException
    {
        public EnCryptoException(string msg) : base(msg) { }
    }

    public class EncryptionWorkEventArgs : EventArgs
    {

        public object oObject;
        public string sFile1;
        public string sFile2;
        public EncryptionWorkEventArgs(object e_oObject, string e_sFile1, string e_sFile2)
        {
            oObject = e_oObject;
            sFile1 = e_sFile1;
            sFile2 = e_sFile2;
        }

    }

    class Encryption : BackgroundWorker 
    {
        private const ulong FC_TAG = 0xFC010203040506CF;
		private const int BUFFER_SIZE = 128*1024;
        public const string sFILE = "_tmpdb";
        private static string sError;

        public Encryption()
        {
            WorkerReportsProgress = true;
            WorkerSupportsCancellation = true;
            sError = "";
        }


        protected override void OnDoWork(DoWorkEventArgs e)
        {
            DoWorkEventArgs oEnc = (DoWorkEventArgs)(e.Argument);
            int i = _OnDoWork((EncryptionWorkEventArgs)oEnc.Argument);
        }
        private static void CleanUp()
        {
            FileInfo oFile = new FileInfo(scicero.ThisPath + sFILE);
            oFile.Delete();
        }

        protected int _OnDoWork(EncryptionWorkEventArgs e) 
        {
            sError = "";
            ReportProgress (0, "Start save/encryption");

            if (e.oObject.GetType().ToString() == "System.IO.MemoryStream")
            {
                EncryptToFile((System.IO.MemoryStream) e.oObject, e.sFile1, e.sFile2);
            }
            else if (e.oObject.ToString() == "CleanUp")
            {
                CleanUp();
            }
            else
            {
                EncryptToFile(e.sFile1, e.sFile2, (string)e.oObject.ToString());
            }
            ReportProgress (100, "Done!");
            return 1;
            //this.OnRunWorkerCompleted(new RunWorkerCompletedEventArgs(Errorcode.NOERROR, (sError != "") ? new Exception(sError) : null, false));
         }


        private static bool CheckByteArrays(byte[] b1, byte[] b2)
		{
			if(b1.Length == b2.Length)
			{
				for(int i = 0; i < b1.Length; ++i)
				{
					if(b1[i] != b2[i])
						return false;
				}
				return true;
			}
			return false;
		}

        private static SymmetricAlgorithm CreateRijndael(string password, byte[] salt)
		{
			PasswordDeriveBytes pdb = new PasswordDeriveBytes(password,salt,"SHA256",1000);
			
			SymmetricAlgorithm sma = Rijndael.Create();
			sma.KeySize = 256;
			sma.Key = pdb.GetBytes(32);
			sma.Padding = PaddingMode.PKCS7;
			return sma;
		}
        private static RandomNumberGenerator rand = new RNGCryptoServiceProvider();

        private static byte[] GenerateRandomBytes(int count)
		{
			byte[] bytes = new byte[count];
			rand.GetBytes(bytes);
			return bytes;
		}

        public delegate void CryptoProgressCallBack(int min, int max, int value);

        public static void EncryptToFile(MemoryStream inStream, string outFile, string password)
        {
            EncryptToFile(inStream, outFile, password, null);
        }

        public static void EncryptToFile(string sInFile, string outFile, string password)
        {
            //TODO: handle exceptions
            FileStream oFileStream;
            try
            {
                oFileStream = new FileStream(@sInFile, FileMode.Open, FileAccess.Read);
            }
            catch (FileNotFoundException)
            {
                sError += "FileNotFound! ";
                return;
            }
            catch (Exception e)
            {
                sError += "Exception (EncryptToFile)! " + e.Message;
                return;
            }
            MemoryStream oMemStream = new MemoryStream((int)oFileStream.Length);
            BinaryReader oBReader = new BinaryReader(oFileStream);
            byte[] oB = oBReader.ReadBytes((int)oFileStream.Length);
            oMemStream.Write(oB, 0, (int)oFileStream.Length);

            EncryptToFile(oMemStream, outFile, password, null);
            oFileStream.Close();
        }


        public static void EncryptToFile(string sInFile, string outFile, string password, CryptoProgressCallBack callback)
        {
            FileStream oFileStream;
            try
            {
                oFileStream = new FileStream(@sInFile, FileMode.Open, FileAccess.Read);
            }
            catch (FileNotFoundException)
            {
                sError += "Filenotfound (EncryptToFile)! ";
                return;
            }
            catch (Exception e)
            {
                sError += "Exception (EncryptToFile)! ";
                return;
            }
            MemoryStream oMemStream = new MemoryStream((int)oFileStream.Length);
            BinaryReader oBReader = new BinaryReader(oFileStream);
            byte[] oB = oBReader.ReadBytes((int)oFileStream.Length);
            oMemStream.Write(oB, 0, (int)oFileStream.Length);

            EncryptToFile(oMemStream, outFile, password, callback);
            oFileStream.Close();
        }

        public static void EncryptToFile(MemoryStream inStream, string outFile,
                    string password, CryptoProgressCallBack callback)
        {
            //FileStream fout = File.Open(outFile, FileMode.Create);
            FileStream fout = File.Open(scicero.ThisPath + sFILE, FileMode.Create);

            long lSize = inStream.Length; // the size of the input file for storing
            int size = (int)lSize;  // the size of the input file for progress
            byte[] bytes = new byte[BUFFER_SIZE]; // the buffer
            int read = -1; // the amount of bytes read from the input file
            int value = 0; // the amount overall read from the input file for progress

            // generate IV and Salt
            byte[] IV = GenerateRandomBytes(16);
            byte[] salt = GenerateRandomBytes(16);

            // create the crypting object
            SymmetricAlgorithm sma = CreateRijndael(password, salt);
            sma.IV = IV;

            // write the IV and salt to the beginning of the file
            fout.Write(IV, 0, IV.Length);
            fout.Write(salt, 0, salt.Length);

            // create the hashing and crypto streams
            HashAlgorithm hasher = SHA256.Create();
            using (CryptoStream cout = new CryptoStream(fout, sma.CreateEncryptor(),
                    CryptoStreamMode.Write),
                      chash = new CryptoStream(Stream.Null, hasher,
                        CryptoStreamMode.Write))
            {
                // write the size of the file to the output file
                BinaryWriter bw = new BinaryWriter(cout);
                bw.Write(lSize);

                // write the file cryptor tag to the file
                bw.Write(FC_TAG);

                // read and the write the bytes to the crypto stream 
                // in BUFFER_SIZEd chunks
                inStream.Seek(0, SeekOrigin.Begin);

                while ((read = inStream.Read(bytes, 0, bytes.Length)) != 0)
                {
                    cout.Write(bytes, 0, read);
                    chash.Write(bytes, 0, read);
                    value += read;
                    if (callback != null)
                        callback(0, size, value);
                }
                // flush and close the hashing object
                chash.Flush();
                chash.Close();

                // read the hash
                byte[] hash = hasher.Hash;

                // write the hash to the end of the file
                cout.Write(hash, 0, hash.Length);

                // flush and close the cryptostream
                cout.Flush();
                cout.Close();
                
            }
            //oZipStream.Close();
            fout.Close();
            //copy the file to the destination
            FileInfo oF = new FileInfo(scicero.ThisPath + sFILE);
            if (File.Exists(scicero.ThisPath + outFile))
            {
                File.SetAttributes(scicero.ThisPath + outFile, FileAttributes.Normal);
            }
            try
            {
                oF.CopyTo(scicero.ThisPath + outFile, true);
            }
            catch (System.IO.IOException eIo)
            {
                //TODO: create handling for this.
                sError += "IOException (EncryptToFile)! ";
                return;
            }
            catch (Exception ex)
            {
                sError += "Exception (EncryptToFile)! ";
                return;
            }
            //delete the old file
            oF.Delete();
            File.SetAttributes(scicero.ThisPath + outFile, FileAttributes.ReadOnly);

        }

        public static void DecryptToStream(string inFile, ref MemoryStream outStream, string password)
		{
            DecryptToStream(inFile, ref outStream, password, null);
        }

        /// <summary>
		/// takes an input file and decrypts it to the output file
		/// </summary>
		/// <param name="inFile">the file to decrypt</param>
		/// <param name="outFile">the to write the decrypted data to</param>
		/// <param name="password">the password used as the key</param>
		/// <param name="callback">the method to call to notify of progress</param>
		public static void DecryptToStream(string inFile, ref MemoryStream outStream, string password, CryptoProgressCallBack callback)
		{
			// NOTE:  The encrypting algo was so much easier...

            // create and open the file streams
			using(FileStream fin = File.OpenRead(inFile))
			{
            	int size = (int)fin.Length; // the size of the file for progress notification
				byte[] bytes = new byte[BUFFER_SIZE]; // byte buffer
				int read = -1; // the amount of bytes read from the stream
				int value = 0;
				int outValue = 0; // the amount of bytes written out

				// read off the IV and Salt
				byte[] IV = new byte[16];
				fin.Read(IV,0,16);
				byte[] salt = new byte[16];
				fin.Read(salt,0,16);
				
				// create the crypting stream
				SymmetricAlgorithm sma = CreateRijndael(password,salt);
				sma.IV = IV;

				value = 32; // the value for the progress
				long lSize = -1; // the size stored in the input stream
				
				// create the hashing object, so that we can verify the file
				HashAlgorithm hasher = SHA256.Create();

				// create the cryptostreams that will process the file
				using(CryptoStream cin = new CryptoStream(fin,sma.CreateDecryptor(),CryptoStreamMode.Read),
						  chash = new CryptoStream(Stream.Null,hasher,CryptoStreamMode.Write))
				{
					// read size from file
					BinaryReader br = new BinaryReader(cin);
					lSize = br.ReadInt64();
					ulong tag = br.ReadUInt64();
					
					if(FC_TAG != tag)
                        throw new EnCryptoException("File Corrupted!");
					
					//determine number of reads to process on the file
					long numReads = lSize / BUFFER_SIZE;

					// determine what is left of the file, after numReads
					long slack = (long)lSize % BUFFER_SIZE;
					
					// read the buffer_sized chunks
					for(int i = 0; i < numReads; ++i)
					{
						read = cin.Read(bytes,0,bytes.Length);
						outStream.Write(bytes,0,read);
						chash.Write(bytes,0,read);
						value += read;
						outValue += read;
                        if (callback != null)
						    callback(0,size,value);
					}

					// now read the slack
					if(slack > 0)
					{
						read = cin.Read(bytes,0,(int)slack);
						outStream.Write(bytes,0,read);
						chash.Write(bytes,0,read);
						value += read;
						outValue += read;
                        if (callback != null)
						    callback(0,size,value);
					}
					// flush and close the hashing stream
					chash.Flush();
					chash.Close();

					// flush and close the output file
					//outStream.Flush();
                    //outStream.Close();

					// read the current hash value
					byte[] curHash = hasher.Hash;

					// get and compare the current and old hash values
					byte[] oldHash = new byte[hasher.HashSize / 8];
					read = cin.Read(oldHash,0,oldHash.Length);
					if((oldHash.Length != read) || (!CheckByteArrays(oldHash,curHash)))
                        throw new EnCryptoException("File corrupted!");
				}
				
				// make sure the written and stored size are equal
				if(outValue != lSize)
                    throw new EnCryptoException("File sizes don't match!");
			}
		}

        public static string DecryptFile(string inFile, string password)
        {
            StringBuilder sb = new StringBuilder();

            // create and open the file streams
            using (FileStream fin = File.OpenRead(inFile))
            {
                long lSize = -1;
                int read = -1;
                int value = 32;
                int outValue = 0;

                byte[] bytes = new byte[BUFFER_SIZE];

                byte[] IV = new byte[16];
                byte[] salt = new byte[16];

                fin.Read(IV, 0, 16);
                fin.Read(salt, 0, 16);

                SymmetricAlgorithm sma;

                HashAlgorithm hasher = SHA256.Create();

                sma = CreateRijndael(password, salt);
                sma.IV = IV;

                using (CryptoStream cin = new CryptoStream(fin, sma.CreateDecryptor(), CryptoStreamMode.Read), chash = new CryptoStream(Stream.Null, hasher, CryptoStreamMode.Write))
                {
                    BinaryReader br;

                    ulong tag;

                    br = new BinaryReader(cin);
                    lSize = br.ReadInt64();

                    if ((tag = br.ReadUInt64()) != FC_TAG)
                    {
                        throw new EnCryptoException("File Corrupted!");
                    }

                    ASCIIEncoding enc = new System.Text.ASCIIEncoding();

                    long numReads = lSize / BUFFER_SIZE;
                    long slack = (long)lSize % BUFFER_SIZE;

                    for (int i = 0; i < numReads; ++i)
                    {
                        string s;

                        read = cin.Read(bytes, 0, bytes.Length);
                        s = enc.GetString(bytes, 0, read);

                        sb.Append(s);

                        chash.Write(bytes, 0, read);

                        value += read;
                        outValue += read;
                    }

                    // now read the slack
                    if (slack > 0)
                    {
                        string s;

                        read = cin.Read(bytes, 0, (int)slack);
                        s = enc.GetString(bytes, 0, read);

                        sb.Append(s);

                        chash.Write(bytes, 0, read);

                        value += read;
                        outValue += read;
                    }

                    // flush and close the hashing stream
                    chash.Flush();
                    chash.Close();

                    byte[] curHash = hasher.Hash;
                    byte[] oldHash = new byte[hasher.HashSize / 8];

                    int ff = oldHash.Length;

                    read = cin.Read(oldHash, 0, oldHash.Length);
                    /*
                    if ((oldHash.Length != read) || (CheckByteArrays(oldHash, curHash)) == false)
                    {
                        throw new EnCryptoException("File Corrupted!");
                    }
                     * */
                }

                // make sure the written and stored size are equal
                if (outValue != lSize)
                {
                    throw new EnCryptoException("File sizes don't match!");
                }

                return sb.ToString();
            }
        }

 
	}


}
